package com.moralesce.gt.model;

import static com.moralesce.gt.util.ListUtil.sort;
import static java.lang.Integer.valueOf;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.moralesce.gt.util.FileUtil;
import com.moralesce.gt.util.ListUtil;

/**
 * A subset of cards held at one time by a player during a game.
 * 
 * A hand consists of exactly five cards, and can be created in one of two ways:<br/>
 * <br/>
 * 
 * 1. By using card collections<br/>
 * A hand can be created by providing a list or array of Cards.<br/>
 * Card[] cards = new Card[] {...};<br/>
 * Hand hand = Hand.newInstance(cards);</br>
 * 
 * List<Card> cards = new ArrayList<Card>();<br/>
 * ... <br/>
 * Hand hand = Hand.newInstance(cards);<br/>
 * <br/>
 * 
 * 2. By using card values<br/>
 * Hand hand = Hand.newInstance("ACKCJS9H4H");
 * 
 * @author Brandon Grenier
 */
public final class Hand implements Comparable<Hand> {

	private final Integer key;
	private final Integer id;
	private final String value;
	private final List<Card> cards;
	private HandStrength strength;

	public static Hand newInstance(String value) {
		if (value.length() != 10) {
			throw new IllegalArgumentException("A hand must be created with a 10 character value");
		}

		List<Card> cards = new ArrayList<Card>();
		cards.add(new Card(String.valueOf(value.charAt(0)) + String.valueOf(value.charAt(1))));
		cards.add(new Card(String.valueOf(value.charAt(2)) + String.valueOf(value.charAt(3))));
		cards.add(new Card(String.valueOf(value.charAt(4)) + String.valueOf(value.charAt(5))));
		cards.add(new Card(String.valueOf(value.charAt(6)) + String.valueOf(value.charAt(7))));
		cards.add(new Card(String.valueOf(value.charAt(8)) + String.valueOf(value.charAt(9))));
		return newInstance(cards);
	}

	public static Hand newInstance(List<Card> cards) {
		return newInstance(cards.toArray(new Card[cards.size()]));
	}

	public static Hand newInstance(Card[] cards) {
		return new Hand(cards);
	}

	private Hand(List<Card> cards) {
		if (cards.size() != 5) {
			throw new IllegalArgumentException("Cannot create a hand with " + cards.size() + " cards - a hand must have 5 cards.");
		}
		if (ListUtil.containsDuplicates(cards)) {
			throw new IllegalArgumentException("There are duplicate cards in the list.");
		}
		this.cards = sort(cards);

		StringBuilder builder = new StringBuilder();
		for (Card card : cards) {
			builder.append(card.value() + ",");
		}
		builder.deleteCharAt(builder.lastIndexOf(","));
		this.value = builder.toString();

		Integer i = 1;
		for (Card card : cards) {
			i = i * card.id();
		}
		this.id = i;

		Integer j = 1;
		for (Card card : cards) {
			j = j * card.rank().id();
		}
		this.key = this.hasFlush() ? j * 67 : j;

		this.strength = HAND_STRENGTH_MAP.get(key);
	}

	private Hand(Card[] cards) {
		this(Arrays.asList(cards));
	}

	/**
	 * Returns the hand id.
	 * 
	 * @return The hand id.
	 */
	public Integer id() {
		return id;
	}

	/**
	 * Returns the hand key.
	 * 
	 * @return The hand key.
	 */
	public Integer key() {
		return key;
	}

	/**
	 * Returns the hand value.
	 * 
	 * @return The hand value.
	 */
	public String value() {
		return value;
	}

	/**
	 * Returns the hand strength as a score from 1 to 7462.
	 * 
	 * The score range is from 1 (highest ranking hand) to 7462 (lowest ranking
	 * hand).
	 * 
	 * @return The hand strength as a score from 1 to 7462.
	 */
	public Integer score() {
		return strength.strength();
	}

	/**
	 * Returns the probability of being dealt this hand, expressed as a
	 * percentage.
	 * 
	 * @return The probability of being dealt this hand, expressed as a
	 * percentage.
	 */
	public Double probability() {
		return strength.probability();
	}

	/**
	 * Returns the hand name.
	 * 
	 * @return The hand name.
	 */
	public String name() {
		if (strength == null) {
			System.out.println("NONE FOR " + value);
		}
		return strength.name();
	}

	/**
	 * Returns the hand type.
	 * 
	 * @return The hand type.
	 */
	public HandType type() {
		return HandType.forValue(strength.type());
	}

	/**
	 * Returns all cards contained in the hand.
	 * 
	 * @return A List<Card> of all cards contained in the hand.
	 */
	public List<Card> cards() {
		return Collections.unmodifiableList(cards);
	}

	/**
	 * Determines if this hand has a straight flush.
	 * 
	 * <p>
	 * A straight flush is a hand that contains five cards in sequence, all of
	 * the same suit, such as Q♣ J♣ 10♣ 9♣ 8♣. Aces can play low in straights
	 * and straight flushes: 5♦ 4♦ 3♦ 2♦ A♦ is a 5-high straight flush.
	 * </p>
	 * 
	 * <p>
	 * There are 40 possible straight flushes, including the four royal flushes.
	 * The probability of being dealt one in a five-card deal is 0.0015%.
	 * </p>
	 * 
	 * @return true if this hand has a straight flush, false otherwise.
	 */
	public boolean hasStraightFlush() {
		return "SF".equals(strength.type());
	}

	/**
	 * Determines if this hand has four of a kind.
	 * 
	 * <p>
	 * A four of a kind is a hand that contains all four cards of one rank and
	 * any other (unmatched) card, such as 9♣ 9♠ 9♦ 9♥ J♥.
	 * </p>
	 * 
	 * <p>
	 * There are 624 possible four of a kinds. The probability of being dealt
	 * one in a five-card deal is 0.024%.
	 * </p>
	 * 
	 * @return true if this hand has four of a kind, false otherwise.
	 */
	public boolean hasFourOfAKind() {
		return "4K".equals(strength.type());
	}

	/**
	 * Determines if this hand has a full house.
	 * 
	 * <p>
	 * A full house is a hand that contains three matching cards of one rank and
	 * two matching cards of another rank, such as 3♣ 3♠ 3♦ 6♣ 6♥.
	 * </p>
	 * 
	 * <p>
	 * There are 3,744 possible full houses. The probability of being dealt one
	 * in a five-card hand is 0.14%.
	 * </p>
	 * 
	 * 
	 * @return true if this hand has a full house, false otherwise.
	 */
	public boolean hasFullHouse() {
		return "FH".equals(strength.type());
	}

	/**
	 * Determines if this hand has a flush.
	 * 
	 * <p>
	 * A flush is a hand where all five cards are of the same suit, but not in
	 * sequence, such as Q♣ 10♣ 7♣ 6♣ 4♣.
	 * </p>
	 * 
	 * <p>
	 * There are 5,148 possible flushes, of which 40 are also straight flushes.
	 * The probability of being dealt a flush that is not also a straight flush
	 * in a five-card hand is 0.20%.
	 * </p>
	 * 
	 * @return true if this hand has a flush, false otherwise.
	 */
	public boolean hasFlush() {
		for (int i = 0; i < cards.size() - 1; i++) {
			if (!cards.get(i).suit().equals(cards.get(i + 1).suit())) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Determines if this hand has a straight.
	 * 
	 * <p>
	 * A straight is a hand that contains five cards of sequential rank in at
	 * least two different suits, such as Q♣ J♠ 10♠ 9♥ 8♥.
	 * </p>
	 * 
	 * <p>
	 * There are 10,240 possible straights, of which 40 are also straight
	 * flushes. The probability of being dealt a straight that is not also a
	 * straight flush in a five-card deal is 0.39%.
	 * </p>
	 * 
	 * @return true if this hand has a straight, false otherwise.
	 */
	public boolean hasStraight() {
		return "S".equals(strength.type());
	}

	/**
	 * Determines if this hand has three of a kind.
	 * 
	 * <p>
	 * A hand that contains three cards of the same rank, plus two cards which
	 * are not of this rank nor the same as each other, such as 2♦ 2♠ 2♣ K♠ 6♥.
	 * </p>
	 * 
	 * <p>
	 * There are 54,912 possible three of a kind hands that are not also full
	 * houses or four of a kind. The probability of being dealt one in a
	 * five-card deal is 2.1%.
	 * </p>
	 * 
	 * 
	 * @return true if this hand has three of a kind, false otherwise.
	 */
	public boolean hasThreeOfAKind() {
		return "3K".equals(strength.type());
	}

	/**
	 * Determines if this hand has two pair.
	 * 
	 * <p>
	 * A hand that contains two cards of the same rank, plus two cards of
	 * another rank (that match each other but not the first pair), plus any
	 * card not of either rank, such as J♥ J♣ 4♣ 4♠ 9♥.
	 * </p>
	 * 
	 * <p>
	 * There are 123,552 possible two pair hands that are not also full houses.
	 * The probability of being dealt one in a five-card deal is 4.75%.
	 * </p>
	 * 
	 * @return true if this hand has two pair, false otherwise.
	 */
	public boolean hasTwoPair() {
		return "2P".equals(strength.type());
	}

	/**
	 * Determines if this hand has one pair.
	 * 
	 * <p>
	 * One pair is a hand that contains two cards of one rank, plus three cards
	 * which are not of this rank nor the same as each other, such as 4♥ 4♠ K♠
	 * 10♦ 5♠.
	 * </p>
	 * 
	 * <p>
	 * There are 1,098,240 possible one pair hands. The probability of being
	 * dealt one in a five-card deal is 42.25%.
	 * </p>
	 * 
	 * @return true if this hand has one pair, false otherwise.
	 */
	public boolean hasPair() {
		return "1P".equals(strength.type());
	}

	/**
	 * Determines if this hand is a high card.
	 * 
	 * <p>
	 * A hand made of any five cards not meeting any of the above requirements,
	 * such as K♥ J♥ 8♣ 7♦ 4♠. Essentially, no hand is made, and the only thing
	 * of any potential meaning in the player's hand is their highest card.
	 * </p>
	 * 
	 * <p>
	 * Of the 2,598,960 possible hands, 1,302,540 do not contain any pairs and
	 * are neither straights nor flushes. As such, the probability of being
	 * dealt a high card hand in a five-card deal is 50.0%.
	 * </p>
	 * 
	 * @return true if this hand is a high card, false otherwise.
	 */
	public boolean hasHighCard() {
		return "HC".equals(strength.type());
	}

	/**
	 * Determines if this hand contains the given cards.
	 * 
	 * @param cards The List<Card> of cards to check for.
	 * 
	 * @return true if this hand contains all of the given cards, false
	 * otherwise.
	 */
	public boolean contains(List<Card> cards) {
		return this.cards.containsAll(cards);
	}

	public int compareTo(Hand hand) {
		return this.score() - hand.score();
	}

	public int hashCode() {
		return id;
	}

	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (!(obj instanceof Hand)) {
			return false;
		}
		Hand hand = (Hand) obj;
		return hand.id().equals(this.id());
	}

	public String toString() {
		return "name=>" + (strength == null ? "null" : strength.name()) + ", value=>" + value + ", strength=>" + (strength == null ? "null" : strength.strength()) + ", id=>" + (id == null ? "null" : id);
	}

	private static final Map<Integer, HandStrength> HAND_STRENGTH_MAP = new HashMap<Integer, HandStrength>();

	static {
		for (String row : FileUtil.readLines("hands.csv")) {
			String[] elements = row.split(",");
			HAND_STRENGTH_MAP.put(valueOf(elements[1]), new HandStrength(valueOf(elements[0]), elements[3], elements[4], Double.valueOf(elements[5])));
		}
	}

}
